package com.googlecode.curvedmetromaps.core;

import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import com.googlecode.curvedmetromaps.core.model.MetroLine;
import com.googlecode.curvedmetromaps.core.model.MetroMap;
import com.googlecode.curvedmetromaps.core.model.MetroStation;

import playn.core.AssetWatcher;
import playn.core.Json;
import playn.core.PlayN;
import playn.core.util.Callback;

public class MapLoader {

	/**
	 * Creates a MetroMap instance from a file path.
	 * @param path JSON or OSM XML file path.
	 * @param callback Callback that gets called when the map is loaded.
	 * @see <a href="http://wiki.openstreetmap.org/wiki/OSM_XML">OSM XML</a>
	 */
	public static void load(final String path, final Callback<MetroMap> callback) {
		// Object that is given to the callback function
		final MetroMap map = new MetroMap();

		// load the map
		PlayN.assets().getText(path, new Callback.Chain<String>(callback) {
			@Override
			public void onSuccess(String resource) {
				// create an asset watcher that will call our callback when all
				// assets are loaded
				AssetWatcher watcher = createAssetWatcher(map, callback);

				String ext = path.substring(path.lastIndexOf('.') + 1);
				ext = ext.toLowerCase();

				if (ext.equals("json")) {
					// Parse text to JSON
					Json.Object doc = PlayN.json().parse(resource);
					// Parse JSON to MetroMap
					parseJson(map, doc);
				} else if (ext.equals("xml")) {
					DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
					DocumentBuilder builder;
					try {
						builder = factory.newDocumentBuilder();
						// reader = new StringReader(resource);
						InputSource input = new InputSource(new StringReader(resource));
						// Parse text to XML
						Document doc = builder.parse(input);
						// Parse OSM XML to MetroMap
						parseOsmXml(map, doc);
					} catch (ParserConfigurationException e) {
						e.printStackTrace();
					} catch (SAXException e) {
						e.printStackTrace();
					} catch (IOException e) {
						e.printStackTrace();
					}
				} else {
					callback.onFailure(new Exception("Failed to recognize map filetype."));
					return;
				}

				// start watcher (will call callback when everything is loaded)
				watcher.start();
			}
		});
	}

	private static void parseJson(final MetroMap metroMap, Json.Object doc) {
		// parse metrostations into a hashmap
		HashMap<String, MetroStation> metroStations = new HashMap<String, MetroStation>();
		Json.Array jsonStations = doc.getArray("stations");
		for (int i = 0; i < jsonStations.length(); i++) {
			Json.Object jsonStation = jsonStations.getObject(i);
			String id = jsonStation.getString("id");
			float x = jsonStation.getNumber("x");
			float y = jsonStation.getNumber("y");

			metroStations.put(id, new MetroStation(x, y, id));
		}

		// parse metrolines
		Json.Array jsonLines = doc.getArray("lines");
		HashMap<String, MetroLine> metroLines = new HashMap<String, MetroLine>();
		for (int i = 0; i < jsonLines.length(); i++) {
			Json.Object jsonLine = jsonLines.getObject(i);
			String id = jsonLine.getString("id");
			Json.Array stations = jsonLine.getArray("stations");

			// Read stations
			ArrayList<MetroStation> stationList = new ArrayList<MetroStation>();
			for (int j = 0; j < stations.length(); j++) {
				String str = stations.getString(j);
				MetroStation m = metroStations.get(str);
				stationList.add(m);
			}

			// Convert list of stations to a line
			metroLines.put(id, new MetroLine(stationList));
		}

		// Create MetroMap from collections of stations and lines.
		metroMap.initialize(metroStations.values(), metroLines.values());
	}

	private static void parseOsmXml(final MetroMap metroMap, Document doc) {
		// parse metrostations into a hashmap
		HashMap<String, MetroStation> metroStations = new HashMap<String, MetroStation>();
		NodeList stationNodes = doc.getElementsByTagName("node");
		for (int i = 0; i < stationNodes.getLength(); i++) {
			Node stationNode = stationNodes.item(i);
			// Ignore all non-station nodes
			if (isStationNode(stationNode) == false)
				continue;

			// Get id, lng and lat attribute
			NamedNodeMap stationAttributes = stationNode.getAttributes();
			String nodeId = stationAttributes.getNamedItem("id").getNodeValue();
			float nodeLng = Float.parseFloat(stationAttributes.getNamedItem("lon").getNodeValue());
			float nodeLat = Float.parseFloat(stationAttributes.getNamedItem("lat").getNodeValue());

			// Add MetroStation to hashmap
			metroStations.put(nodeId, new MetroStation(nodeLng, nodeLat, nodeId));
		}

		// parse ways into arrays of stations
		HashMap<String, MetroStation> metroStops = new HashMap<String, MetroStation>();
		NodeList relationNodes = doc.getElementsByTagName("relation");
		ArrayList<ArrayList<MetroStation>> stationLists = new ArrayList<ArrayList<MetroStation>>();
		// Iterate through relations
		for (int i = 0; i < relationNodes.getLength(); i++) {
			Node relationNode = relationNodes.item(i);
			if (relationNode.getNodeType() != Node.ELEMENT_NODE)
				continue;

			ArrayList<MetroStation> stationList = new ArrayList<MetroStation>();
			stationLists.add(stationList);
			Element relationElement = (Element) relationNode;
			String relationId = relationElement.getAttribute("id");
			NodeList memberNodes = relationElement.getElementsByTagName("member");
			// Iterate through children of a way
			for (int j = 0; j < memberNodes.getLength(); j++) {
				Node memberNode = memberNodes.item(j);
				if (memberNode.getNodeType() == Node.ELEMENT_NODE) {
					Element memberElement = (Element) memberNode;
					String ref = memberElement.getAttribute("ref");
					String role = memberElement.getAttribute("role");
					if (role.equals("stop") && metroStations.containsKey(ref)) {
						// Line stops at this station
						MetroStation m = metroStations.get(ref);
						stationList.add(m);
						if (metroStops.containsKey(ref) == false)
							metroStops.put(ref, m);
					}
				}
			}

			// HACK: Linea12 is a cycle, fix this
			NodeList tagNodes = relationElement.getElementsByTagName("tag");
			for (int j = 0; j < tagNodes.getLength(); j++) {
				Node tagNode = tagNodes.item(j);
				if (tagNode.getNodeType() == Node.ELEMENT_NODE) {
					Element tagElement = (Element) tagNode;
					String v = tagElement.getAttribute("v");
					if (v.equals("L12")) {
						stationList.add(stationList.get(0));
					}
					else if (v.equals("L6")) {
						stationList.add(stationList.get(0));
					}
				}
			}
		}

		// Convert stationLists to metrolines
		ArrayList<MetroLine> metroLines = new ArrayList<MetroLine>();
		for (ArrayList<MetroStation> stationList : stationLists) {
			metroLines.add(new MetroLine(stationList));
		}

		// Create MetroMap from collections of stations and lines.
		metroMap.initialize(metroStops.values(), metroLines);
	}

	private static boolean isStationNode(Node node) {
		boolean hasRailwayStation = false;

		// Look for tags that indicate node is a station
		NodeList children = node.getChildNodes();
		for (int i = 0; i < children.getLength(); i++) {
			Node child = children.item(i);
			if (child.getNodeType() == Node.ELEMENT_NODE) {
				Element childElement = (Element) child;
				if (childElement.getTagName().equals("tag")) {
					String k = childElement.getAttribute("k");
					String v = childElement.getAttribute("v");
					if (k.equals("railway") && v.equals("station"))
						hasRailwayStation = true;
				}
			}
		}

		return hasRailwayStation;
	}

	/**
	 * Creates an asset watcher that chains the asset loading to a callback.
	 * @return The asset watcher. Call {@link playn.core.AssetWatcher.start()}
	 *         when resource is processed.
	 */
	private static AssetWatcher createAssetWatcher(final MetroMap metroMap, final Callback<MetroMap> callback) {
		return new AssetWatcher(new AssetWatcher.Listener() {
			@Override
			public void done() {
				callback.onSuccess(metroMap);
			}

			@Override
			public void error(Throwable e) {
				callback.onFailure(e);
			}
		});
	}
}
